package nl.flotsam.test;

import org.apache.commons.io.IOUtils;
import org.junit.rules.MethodRule;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.Statement;
import org.mortbay.jetty.*;
import org.mortbay.jetty.bio.SocketConnector;
import org.mortbay.jetty.handler.AbstractHandler;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A simple object allowing you to launch a web server from your test case, and have it return a predefined resource.
 * The class implements {@link org.junit.rules.MethodRule}, so if you create it using JUnit's support for {@linkplain
 * org.junit.Rule @Rule}, then JUnit will make sure this WebResourceServer will get a chance to start itself at the beginning of
 * a test, and stop itself at the end of the test. <p/> <p> In order to use this class, simply define a <em>public</em>
 * field in your test, and assign it a new instance of this class. The annotate your test methods with {@linkplain
 * WebResource @WebResource} annotations to specify the source and mimetype of the content the Web
 * Server needs to return. </p>
 */
public class WebResourceServer implements MethodRule {

    /**
     * The port number on which the server will be running.
     */
    private final int portNumber;

    /**
     * The number of requests sent to the server.
     */
    private AtomicInteger numberOfHits = new AtomicInteger();

    /**
     * Constructs a new instance, accepting the environment variable used to figure out which port to use, and a default
     * portnumber, to use in case the environment variable is not set. This constructor is added here to make sure it
     * integrates nicely with continuous integration servers such as Hudson. (This is particularly convenient if you
     * have multiple builds running on the same server, and you need to prevent port conflicts.)
     */
    public WebResourceServer(String envVariableName, int defaultPortNumber) {
        String value = System.getenv(envVariableName);
        portNumber = value != null ? Integer.parseInt(value) : defaultPortNumber;
    }

    /**
     * Constructs a new instance, accepting the port to use.
     */
    public WebResourceServer(int portNumber) {
        this.portNumber = portNumber;
    }

    /**
     * Returns the URL to connect to. (This basically prevents you from having to track which port number was choosen.
     * Again, particularly useful in case you have the port number being passed in by the continuous integration
     * server.)
     */
    public String getURL() {
        return "http://localhost:" + portNumber + "/";
    }

    public Statement apply(final Statement base, FrameworkMethod method, Object target) {
        WebResource webResource = method.getAnnotation(WebResource.class);
        if (webResource != null) {
            Resource resource =
                    new DefaultResourceLoader(target.getClass().getClassLoader()).getResource(webResource.content());
            final Server server = createServer(resource, webResource.contentType(), portNumber);
            return new Statement() {

                @Override
                public void evaluate() throws Throwable {
                    server.start();
                    try {
                        base.evaluate();
                    } finally {
                        server.stop();
                    }
                }
            };
        } else {
            return base;
        }
    }

    private Server createServer(final Resource resource, final String contentType, int portNumber) {
        Server server = new Server();
        Connector connector = new SocketConnector();
        connector.setPort(portNumber);
        server.setConnectors(new Connector[]{connector});
        Handler handler = new AbstractHandler() {

            public void handle(String s, HttpServletRequest httpServletRequest, HttpServletResponse response, int dispatch) throws IOException, ServletException {
                numberOfHits.incrementAndGet();
                Request request = (httpServletRequest instanceof Request) ? (Request) httpServletRequest : HttpConnection.getCurrentConnection().getRequest();
                request.setHandled(true);
                response.setContentType(contentType);
                response.setStatus(HttpServletResponse.SC_OK);
                InputStream in = resource.getInputStream();
                try {
                    IOUtils.copy(in, response.getOutputStream());
                } finally {
                    IOUtils.closeQuietly(in);
                }
            }
        };
        server.setHandler(handler);
        return server;
    }

    /**
     * Returns the number of hits ending up at the server.
     */
    public int getNumberOfHits() {
        return numberOfHits.intValue();
    }

}
